package org.mame.builders;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NavigableSet;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Vector;

import javax.swing.JOptionPane;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * BuildROMConfigs uses the given property file to search for and sort a list of game files, creating the
 * configuraiton files that will allow the games to be read by the HyperSpin frontend
 * 
 * @author Bill
 * @version 1.0.2
 *
 */
public class BuildRomConfigs {
	// Globals variables
	private static final String propFileStr                  = "config" + File.separator + "BuildersTest.properties";
	private static Logger log                                = LogManager.getLogger(BuildRomConfigs.class.getName());
	private static int processCount                          = 0;
	private static int configCount                           = 0;
	private static String rom_dir_str                        = null;
	private static String base_dir_str                       = null;
	private static String output_dir_str                     = null;
	private static String output_file_name                   = null;
	private static String rom_exts_str                       = null;
	private static String list_name                          = null;
	private static String list_version                       = null;
	private static boolean searchSubFolders                  = false;
	private static StringBuffer config                       = new StringBuffer();
	private static Vector<File> rom_list                     = new Vector<File>();
	private static Properties props = new Properties();
	private static TreeMap<String, Vector<File>> sorted_list = new TreeMap<String, Vector<File>>(); 

	// Constant variables
	private static final String PROPERTY_BASE_DIRECTORY      = "base_dir";
	private static final String PROPERTY_ROM_DIR             = "rom_dir";
	private static final String PROPERTY_OUTPUT_DIR          = "output_dir";
	private static final String PROPERTY_OUTPUT_FILE_NAME    = "rom_config_output_filename";
	private static final String PROPERTY_ROM_EXTENSIONS      = "rom_extensions";
	private static final String PROPERTY_INCLUDE_SUB_DIRS    = "include_sub_dirs";
	private static final String PROPERTY_LIST_NAME           = "console_name";
	private static final String PROPERTY_LIST_VERSION        = "list_version";
	private static final String OUTPUT_FILE_DEFAULT          = "output.xml";
	private static final String SUB_DIRS_DEFAULT             = "false";
	private static final String ROM_EXTENSIONS_DELINEATOR    = "|";
	private static final String CLASS_VERSION                = "1.0.1";
	
	// Error codes
	private static final int ERROR_CONFIGURATION_FILE_ISSUE  = 1;
	private static final int ERROR_PROPERTY_VALUE_REQUIRED   = 2;
	private static final int ERROR_SEARCH_BASE_INVALID       = 3;
	private static final int ERROR_OUTPUT_FILE_FAILURE       = 4;
	
	/**
	 * Creates the config file property file in the String[]
	 * 
	 * @param args- element 0 contains the absolute path to a property file showing where to look for games, media, hyperspin, etc
	 * 
	 * @return 0 if successful, >0 otherwise
	 */
	public static int buildConfig(String[] args){
		File propFile;
		
		// Get the property file, either passed in or from the default location
		try {
			if(args != null && args.length > 0){
				propFile = new File(args[0]);
			}else{
				propFile = new File(propFileStr);
			}
			// File must exist to open it, DUH!
			if(!propFile.exists()){
				log.error("Configuration file "+ propFile.getAbsolutePath() + " not found");
				return(ERROR_CONFIGURATION_FILE_ISSUE);
			}
			props.load(new BufferedInputStream(new FileInputStream(propFile)));
			
			log.debug("Properties Output:\n" + props.toString());
			
			// Get properties needed for this class
			rom_dir_str = props.getProperty(PROPERTY_ROM_DIR);
			base_dir_str = props.getProperty(PROPERTY_BASE_DIRECTORY);
			output_dir_str = props.getProperty(PROPERTY_OUTPUT_DIR);
			output_file_name = props.getProperty(PROPERTY_OUTPUT_FILE_NAME, OUTPUT_FILE_DEFAULT);
			searchSubFolders = new Boolean(props.getProperty(PROPERTY_INCLUDE_SUB_DIRS, SUB_DIRS_DEFAULT)).booleanValue();
			list_name = props.getProperty(PROPERTY_LIST_NAME);
			list_version = props.getProperty(PROPERTY_LIST_VERSION);
			rom_exts_str = props.getProperty(PROPERTY_ROM_EXTENSIONS);
		} catch (FileNotFoundException e) {
			System.out.println("Properties file " + propFileStr + " cannot be found");
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} 
		
		// Couple null checks for those values that are required
		if(null == rom_dir_str){
			log.error("Rom Directory cannot be null!");
			return(ERROR_PROPERTY_VALUE_REQUIRED);
		}else if(null == base_dir_str){
			log.error("Base search directory cannot be null!");
			return(ERROR_PROPERTY_VALUE_REQUIRED);
		}else if(null == output_dir_str){
			log.error("Output direcory cannot be null!");
			return(ERROR_PROPERTY_VALUE_REQUIRED);
		}
		
		// Create a list of valid ROM files in the given directory
		getRomFileList(props);

		log.info("Processed " + processCount + " ROM files");
		
		// Create an XML String of all the ROM files formatted as HyperSpin likes
		buildXMLData();

		// Throw the XML onto the file system
		outputFile();
		
		log.info("Wrote " + configCount + " ROM files");
		
		// Un-load the properties file for next run
		props = null;
		
		return 0;
	}

	/*
	 * Writes the StringBuffer to the file system at the location found in the property file
	 */
	private static void outputFile() {
		log.debug("Starting to create the output file using path: " + output_dir_str + " and name: " + output_file_name);
		
		// Got to have an xml file, append .xml if it's not already there
		if(!output_file_name.endsWith(".xml")){
			log.info("Appending .xml to supplied output file name");
			output_file_name += ".xml";
			log.debug("Output file name updated to " + output_file_name);
		}
		
		// Jsut delete the old file if one existed.  It's useless anyway
		File output = new File(output_dir_str + File.separator + output_file_name);
		if(output.exists()){
			output.delete();
		}
		try{
			output.createNewFile();
			
			// Throw the XML String out to the newly created file
			FileWriter fw = new FileWriter(output.getAbsoluteFile());
			BufferedWriter bw = new BufferedWriter(fw);
			bw.write(config.toString());
			bw.close();
		}catch (IOException ioe){
			log.error("Error creating output file " + output.getAbsolutePath());
			System.exit(ERROR_OUTPUT_FILE_FAILURE);
		}
			
		log.info("Output file " + output.getAbsolutePath() + " written successfully");
		
		return;
	}

	/*
	 * Creates the XML in a StringBuffer for each game file found
	 * @return the StringBuffer containing the XML data
	 */
	private static StringBuffer buildXMLData() {
		File rom;
		String name = "";
		
		// Date for the header data
		DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy");
		Calendar date = Calendar.getInstance();
		
		// Header data for XML file
		config.append("<?xml version=\"1.0\"?>\n");
		config.append("<menu>\n");
		config.append("\t<header>\n");
		config.append("\t\t<listname>" + list_name + "</listname>\n");
		config.append("\t\t<lastlistupdate>" + dateFormat.format(date.getTime()) + "</lastlistupdate>\n");
		config.append("\t\t<listversion>" + list_version + "</listversion>\n");
		config.append("\t\t<exporterversion>" + BuildRomConfigs.class.getName() + " v" + CLASS_VERSION + "</exporterversion>\n");
		config.append("\t</header>\n");

		NavigableSet<String> keys = sorted_list.navigableKeySet();
		Iterator<String> it = keys.iterator();
		String key;
		String index; // Will be set to true for those entries that get indexed; the first entry of each letter
		String image; // Will be equal to the key for when index is true
		
		while(it.hasNext()){
			key = it.next();
			log.debug("Building key " + key);
			rom_list = sorted_list.get(key);
			// Sort the list so each list is alpha-numerically sorted
			sortRomList();
						
			// Go through each ROM file found and create an entry for it	
			for(int i=0; i<rom_list.size(); i++){
				index = ""; // Blank this out, will be set if needed next
				image = ""; // Blank this out, will be set if needed next
				
				if(i==0){
					index = "true";
					image = key;
				}
				
				rom = rom_list.get(i);
				name = rom.getName().substring(0, rom.getName().lastIndexOf("."));
				log.info("Building XML data for " + rom.getName());
				
				config.append("\t<game name=\"" + name + "\" index=\"" + index + "\" image=\"" + image + "\">\n");
				config.append("\t\t<description>" + name + "</description>\n");
				config.append("\t\t<cloneof></cloneof>\n");
				config.append("\t\t<crc></crc>\n");
				config.append("\t\t<manufacturer></manufacturer>\n");
				config.append("\t\t<year></year>\n");
				config.append("\t\t<genre></genre>\n");
				config.append("\t\t<rating></rating>\n");
				config.append("\t\t<enabled>yes</enabled>\n");
				config.append("\t</game>\n");
				configCount++;
			}	
			
			rom_list = null;  // Just to clean up, make sure it's not set for future work
		}
		
		// Close out the XML data
		config.append("</menu>\n");
		
		return config;
	}

	/*
	 * Takes the game list and sorts it so the listing in HyperSpin will be alphabetical
	 */
	private static void sortRomList() {
		// Call the Collections.sort method with a custom implementation of the Comparator class for File compares
		Collections.sort(rom_list, new Comparator<File>() {
			@Override
			public int compare(File f1, File f2){
				return f1.getName().compareTo(f2.getName());
			}
		});
	}

	/**
	 * Searches the rom path given in the property file and builds a TreeMap of valid game files
	 * The TreeMap key is the first character of the game name (0-9,a-z) while the 
	 * value is a Vector of Files referencing each game.  This makes indexing simple later on
	 * 
	 * @param props- Property file containing the information required
	 *
	 * @return TreeMap containing Vectors of Files denoting each game to add to the configuraiton
	 */
	protected static TreeMap<String, Vector<File>> getRomFileList(Properties props) {
		Vector<String> exts = new Vector<String>();
		StringTokenizer extsTokens = new StringTokenizer(rom_exts_str, ROM_EXTENSIONS_DELINEATOR);
		String tmp;
		File[] romList;
		File rom_dir;
		
		// Create a list of valid ROM extensions to easily test against- not very memory efficient
		while(extsTokens.hasMoreTokens()){
			tmp = extsTokens.nextToken().trim();
			exts.add(tmp);
			log.debug("Including rom extension " + tmp + " in search criteria");
		}
		
		// Make sure the directory the user thinks exists actually exists.
		rom_dir = new File(rom_dir_str);
		if(!rom_dir.isAbsolute() && !rom_dir.exists()){
			rom_dir = new File(base_dir_str + File.separator + rom_dir_str);
			if(!rom_dir.isAbsolute() && !rom_dir.exists()){
				log.error(rom_dir_str + " is not a valid search location");
				JOptionPane.showMessageDialog(new JOptionPane(), "Rom directory not found, exiting", "", JOptionPane.ERROR_MESSAGE);
				System.exit(ERROR_SEARCH_BASE_INVALID);
			}
		}
		
		if(!rom_dir.isDirectory()){
			log.error(rom_dir_str + " exists but is not a directory");
			JOptionPane.showMessageDialog(new JOptionPane(), "Rom directory " + rom_dir.getAbsolutePath() + " is not a valid directory location, exiting", "", JOptionPane.ERROR_MESSAGE);
			System.exit(ERROR_SEARCH_BASE_INVALID);
		}
		
		// Get a list of all files in the root directory to check
		romList = rom_dir.listFiles();
		log.debug("Processing rom directory " + rom_dir.getAbsolutePath());
		processRomList(romList, exts);
		
		return sorted_list;
	}

	/*
	 * Checks a directory for valid game files, adding games to the process list as necessary
	 * 
	 * @param romList- The directory to search in
	 * @param exts- The list of valid extensions for this system's game files
	 */
	private static void processRomList(File[] romList, Vector<String> exts) {
		String extension = "";
		String filename = "";
		String startingLetter = "";
		
		// Check every file in the directory
		for(int i=0; i<romList.length; i++){
			// For directories, if include sub dirs was set check the folders files
			if(romList[i].isDirectory()){
				if(searchSubFolders){
					log.debug("Processing rom sub-directory " + romList[i].getName());
					processRomList(romList[i].listFiles(), exts);
				}
			// Otherwise see if the file matches a known ROM extension
			}else{
				filename = romList[i].getName();
				log.debug("Processing file: " + filename);
				extension = filename.substring(filename.lastIndexOf(".") + 1);
					
				// It matches! Add to the list of ROMs to create data for
				if(exts.contains(extension)){
					log.info("File " + filename + " being added to rom list");
					
					// Get the first letter, used to put it in the correct place in the HashMap of sorted ROMs
					startingLetter = romList[i].getName().substring(0, 1);
					rom_list = sorted_list.get(startingLetter);
					if(rom_list == null){
						rom_list = new Vector<File>();
					}
					rom_list.add(romList[i]);
					sorted_list.put(startingLetter, rom_list);
					rom_list = null;
					processCount++;
				}else{
					log.debug("File: " + filename + " - Extension: " + extension + " is not part of the valid extensions set");
				}
			}
		}
	}


}
